package com.dycong.common.common.lock.v1;

import com.dycong.common.database.redis.JedisTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

@Service
public class RedisDistributedLockImpl implements RedisDistributedLock {


    @Autowired
    private JedisTemplate jedisTemplate;


    private static final Logger logger = LoggerFactory.getLogger(RedisDistributedLockImpl.class);
    /**
     * 加锁
     * @param lockName  锁的key
     * @param acquireTimeout  获取超时时间
     * @param timeout   锁的超时时间(单位毫秒)
     * @return 锁标识
     */

    @Override
    public String lockWithTimeout(String lockName,
                                  long acquireTimeout, long timeout) {
        try {
            // 随机生成一个value
            String identifier = UUID.randomUUID().toString();
            // 锁名，即key值
            String lockKey = "lock:" + lockName;
            // 超时时间，上锁后超过此时间则自动释放锁
            int lockExpire = (int)(timeout / 1000);
            // 获取锁的超时时间，超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (jedisTemplate.setnx(lockKey, identifier) == 1) {
                    jedisTemplate.expire(lockKey, lockExpire);
                    // 返回value值，用于释放锁时间确认
                    return identifier;
                }
                // 返回-1代表key没有设置超时时间，为key设置一个超时时间
                if (jedisTemplate.ttl(lockKey) == -1) {
                    jedisTemplate.expire(lockKey, lockExpire);
                }
                //避免无意义的空转，给redis过大压力
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            logger.error("==RedisDistributedLockImpl lockWithTimeout error==",e);
            throw e;
        }
        return null;
    }

    // TODO: 2019/1/21 redis通过 MULTI,EXEC,WATCH 等命令 提供了一种将多个命令请求打包，然后一次性、按顺序的执行多个命令的机制，并且在事务执行期间，
    //  服务器不会中断事务而改去执行其他客户端的命令请求 ，它会将事务中的所有命令执行完毕才去处理其他客户端的命令请求。

    // TODO 当一个处于事务状态的客户端向 redis 发送 EXEC 时，redis 会遍历这个客户端的事务队列，执行队列中的所有命令，最后将所得的结果全部返回给客户端。
    //  WATCH 是一个乐观锁，他可以在 EXEC 命令执行之前，监视任意数量的 key ，并在 EXEC 执行时，检查被监视的 key 是否至少有一个已经被修改过了，
    //  如果是的话，服务器将拒绝执行事务，并向客户端返回代表执行失败的空回复。每个redis数据库都保存着一个watched_keys字典，这个字典的key是被WATCH命令
    //  监控的一个key，而字典的值则是一个链表，链表中记录了所有监视相应 key 的客户端。


    //TODO redis 的事务与传统的关系型数据库最大的差别在于，redis 不支持事务回滚机制，即使事务队列中的某个命令在执行期间出现了错误，整个事务也会继续执行下去，
    // 直到将事务队列中的所有命令都执行完毕为止。

    // TODO: 2019/1/21 一致性 Redis 舍弃了回滚的设计，基本上也就舍弃对数据一致性的有效保证。不过对于一个高效的 key-value store 或 data structure server，数据操作一致性
    //  很多时候更多应该依赖应用层面，事实也是我们使用 Redis 时很多时候都是分片和集群的，数据一致性无法依靠任何事务机制。

    // TODO: 2019/1/21 持久性(Durability) Redis 一般情况下都只进行内存计算和操作，持久性无法保证。但 Redis 也提供了2种数据持久化模式，SNAPSHOT 和 AOF，SNAPSHOT的持
    //  久化操作与命令操作是不同步的，无法保证事务的持久性。而AOF模式意味着每条命令的执行都需要进行系统调用操作磁盘写入文件，可以保证持久性(在配置为 always同步命令时才能保证不那么严苛的事务持久性)，但会大大降低 Redis 的访问性能。

    // TODO: 2019/1/21 隔离性(Isolation) 隔离性保证了在事务完成之前，该事务外部不能看到事务里的数据改变。也不能访问一些中间状态，因为假如事务终止的话，这些状态将永远不会发生。
    //  Redis 采用单线程串行化设计，在一个事务完成之前，其他客户端提交的各种操作都无法执行因此自然没法看见事务执行的中间状态，隔离性得到保证。

    // TODO: 2019/1/21 Redis 在2.6版本开始提供脚本（Lua scripting）能力，一种更灵活的批量命令组织方式用于取代目前的事务机制。脚本提供了更强大和灵活的编程能力，但也
    //  是一把双刃剑，由于 Redis 需要保证脚本执行的原子性和隔离性，脚本执行期间会阻塞其他命令的执行，因此建议写一些高效的脚本。不过从开发者的角度来说使用Lua脚本的成本
    //  （学习成本、开发成本、维护成本）都要更大，特别是一些开发者如果将应用的业务逻辑放入脚本中来执行，是不是让人想起了数据库的存储过程。

    /**
     * 释放锁
     * @param lockName 锁的key
     * @param identifier    释放锁的标识
     * @return
     */
    @Override
    public boolean releaseLock(String lockName, String identifier) {
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            for(;;) {
                // 监视lock，准备开始事务
                // Redis Watch 命令用于监视一个(或多个) key ，
                // 如果在事务执行之前这个(或这些) key 被其他命令所改动，那么事务将被打断
                // 这里类似于CAS策略
                jedisTemplate.watch(lockKey);
                // 通过前面返回的value值判断是不是该锁，若是该锁，则删除，释放锁
                if (identifier.equals(jedisTemplate.get(lockKey))) {
                    /*todo multi将客户端从非事务状态切换到事务状态*/
                    Transaction transaction = jedisTemplate.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    // 执行EXEC命令后会取消对所有键的监控
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                jedisTemplate.unwatch();
                break;
            }
        } catch (JedisException e) {
            logger.error("==RedisDistributedLockImpl releaseLock error==",e);
            throw e;
        }
        return retFlag;
    }
}
